avatar

目录
306 - cpp 初始化列表构造函数详解

## C++ 初始化列表构造函数详解

🧑‍💻 User

In this lecture, we’re going to learn about initializer list constructors, and these are constructorsthat are used to initialize what we call aggregates in C plus plus and aggregates are really some kindsof containers and they are special containers in C plus plus and this special kind of containers thatcan contain stuff.For example, you see that we have an outer container which contains some little boxes in here and itis a representation of an aggregate in C plus plus.It is basically something that is going to aggregate a lot of little things together, but it’s notevery container in C plus plus there is a nuanced definition of aggregates, but for now you can thinkof an aggregate as just a container of things.And from the containers we have seen so far, an array is an aggregate and a struct which only has publicmember variables is also an aggregate.So all these aggregates can use a unified syntax to initialize them.For example, we can use the curly braces syntax here to initialize and put data in our container here.And when the compiler sees your initializer here, it knows how to generate the tools it will use toput this data in your container here.So, for example, it might choose to put one in this little box.It might choose to put two in this little box.It might choose to put three in this little box.It might choose to put four in this little box.And whatever we have after this is going to be put in sequential boxes that we have in our box containerhere.This is what a list initializer constructor is going to do.It’s going to be used by the compiler to take the data from our initializer here and put that in ouraggregate compound type.And at this point in the course we really know about to aggregate compound types, we have seen thatstruct is one type of aggregate and an array is also another type of aggregate.So we’re going to see how we can use an initializer list constructor with a struct because an arrayis really a built in type.We can’t mess with that and set up another way to initialize it.So we’re going to do this only for the struct in this course to let you learn about initializer listconstructors.Okay.So here is a simple struct we’re going to use.It’s going to be a point struct.It is going to be modeling a point.Inside we have two member variables.We have double X and double y and they are going to be public because we are using a structure.Members of a struct are public by default here.So we can set up a syntax like this to try and initialize this little guy and we’re going to see thatthis syntax is going to work even if we didn’t do anything to make this work.And what the compiler is going to do behind the scenes, it’s going to generate an initializer listconstructor and it’s going to use it to take the data from our curly braces here and store that in ourpoint object.So it is basically to take the first parameter here and store that in X and it is going to take thesecond parameter and store that in Y here.But sometimes you need to change the behavior of this initializer list constructors and we’re goingto see how we can set up our own and let to do exactly what we want.And here is a simple example of how you can do that in initializer List constructor is going to takethis form here.It’s going to have the name of the constructor, which is the name of the class.But our parameter list is going to take an STD initializer list, which is going to be an instance ofa template class.We haven’t really learned about class templates, but please think about STD initializer list here asan instance of a class template and you see that we initialize this for double because we know thatour class is going to contain double member variables.So these two things have to match the type of your member variables, has to match the type that youpass in your STD initializer list.Instance.Once you have this initializer list parameter, the compiler is going to take what you call in yourmain function and use that to create an STD initializer list object.So what is going to basically happen is if we issue a call like this, the compiler is going to noticethat we are using an initializer list here and it is going to grab whatever we have here and it is goingto call our initializer List constructor.So it is going to create a parameter that it is going to pass to that constructor.And the parameter happens to be an STD initializer list, which is going to be an instance for doubletype.So this is what we’re going to have here.So the compiler is so the compiler is going to take the first parameter and put that in the first spotfor our initializer list here.And it’s going to take the second parameter.And put that in the second spot in our STD initializer list.Instance object here.Okay, so now that we have the data, we need to grab the data and use that in our object and we’regoing to use a concept we haven’t really learnt about to do this.We’re going to use what we call iterators, but we haven’t learnt about them because we don’t have enoughtools to understand them fully yet.But I am going to try and help you understand this.Think of the begin function We call on our initializer list here as a function that is going to giveyou the starting pointer in our collection.So Begin is basically going to be a pointer to the first element in our initializer list here, andend is going to be a pointer past the last element in our initializer list here.So let’s say that Begin is going to be a pointer to the start of our list.It’s going to be a pointer to the first element in the list and end is going to be a pointer past thelast element in our initializer list here.So it’s not going to point to 45.3 here.It’s going to point past the last element.So it’s going to point to the spot past the last element in our initializer list here.So hopefully you have an understanding of what begin and end here are.So we’re going to use this information to our advantage in this loop here.So we’re going to use this to our advantage in this for loop, we’re going to say for auto I, the beginningis going to be the beginning pointer and we’re going to keep looping as long as our pointer is not theend pointer and as long as we have a valid pointer, we’re going to keep incrementing.And if we increment for the first iteration, we’re going to grab the first element in our initializerlist.And for the second iteration we’re going to grab the second element.And once we hit the third iteration, we’re going to be basically at the end of our list and we willstop printing stuff here so we can use this syntax and print information that we have in our initializerlist.And notice that what we use inside our body is the dereferencing operator to dereference this pointerand get to what is pointed to by this iterator pointer here.Okay.So now that we have this, we can even use pointer arithmetic notation to grab data from our iterator.So for example, we can grab what is pointed to by the begin pointer using the syntax here and we cangrab the second element using the syntax here, using pointer arithmetic on our begin pointer here andwe can do this and we’re going to be using this to our advantage to grab data that we store in our membervariables here.So please take a look at our Initializer List constructor here and see what we are doing.We are grabbing data from our initializer list parameter and then the first thing is going to be usedto initialize the x member variable.And the second thing is going to be used to initialize our y member variable.And with this we have successfully grabbed the data from our initializer list and used that to initializeour member variables.This is how you can set this up and realize the begin thing can throw you off a little bit.But please think of this as a pointer to the beginning element in your initializer list collection.Once you understand this like this, you will be able to use your initializer list constructors withouta problem.Okay, Now that you know this, we’re going to head over to Visual Studio code and try this out.Okay.Here we are in our working folder.The current project is Initializer List Constructors.We’re going to grab our template files and we’re going to put them in place and we are going to openthis in Visual Studio code by dragging and dropping here, This is going to open our folder and we’regoing to close the sidebar here and clean up a little bit.We’re going to set up a struct which is going to be our sandbox.So let’s say that it is going to be a point struct and inside we’re going to have two member variables.We’re going to have a double variable for x, the x coordinate of our point and we’re going to havea double variable for our Y coordinate here and we’re going to set up a function which is going to printour point easily.So let’s put a semicolon after here and we’re going to say void print point and we’re going to takethe point by const reference and we’re going to print it out with Stdcout.We’re going to say Stdcout, we’re going to say point and we’re going to grab its X, coordinate andprint that out.We’re going to say p x here and we’re going to grab the Y coordinate and print that out as well.We’re going to say p y.Think we can save that and then we’re going to.Close this off with a square bracket and then we’re going to put our new line character.This is going to do.Now that we have this, we can set up a point in the main function and we’re going to do that like this.We’re going to say point P one and we are going to use a curly brace, aggregate initializer.So we’re going to use this syntax here and we’re going to put data in our point.We’re going to put in a few arguments, let’s say 22.4 and 65.4.We can really put in anything.We just want to see that the data we specify here is going to end up in the object.And after we set up the object, we’re going to print the information in here.So we’re going to say print point and we’re going to pass our P one point and see what happens here.Let’s bolt.We’re going to use JK to do this.You’re going to see the world is going to be good and we’re going to clear and run.Rooster You’re going to see that we have our point here.And notice that this syntax is working.Even if we didn’t really set up any constructor to make this work.So how is this working?Well, the compiler is generating an initializer list constructor behind your back, and that initializerlist constructor is taking the data here and using some magic to take the data and put that in thismember variables.And when you get to use your object, for example, by printing it out here, you’re going to see thatthe data here has ended up in this p one object.What the initializer list is going to do by default is to take the first thing here and use that toinitialize X like this.And then it’s going to take the second thing you specify here and use that to initialize y like this.Or I should say this is what the initializer list constructor that is generated by the compiler is goingto do by default.If we want to see this happening, for example, through a debugger or we want to change this defaultbehavior, for example, we want to use the first thing to initialize y and the second thing to initializeX.Suppose you want that for your application.You will need to set up your own initializer list constructor and we’re going to do that in our structhere.So the way we’re going to do that, we’re going to modify our structure a little bit and we’re goingto put in our member variables.For example, we can make this two guys private and let’s bring this to the left a little bit, andwe’re going to put in a public section.And please note that this is just a preference.If you want, you can leave them as public.But I like to keep my member variables private unless I have a compelling reason not to do so.So we’re going to put in our initializer list constructor.It’s going to be called Point like any other constructor, and then it’s going to take an initializerlist parameter.And this is going to be a template instance for double for the initializer list class template we havein the C plus plus standard library.So this is something that is built in and we just want to use this here.So we’re going to call this list and then we’re going to put a body which is going to be used to passthis list and grab the data and put that in our member variables here, let’s say list properly.Okay.So in this list, we’re going to do things step by step.What we want to do is to put a message out that this constructor was actually called.So we’re going to say initializer list called or constructor called and we’re going to put this out.Okay.So let’s see that this is actually the case.And now you see that because we have made our member variables private, we no longer have access tothem, but we can take this function and make it part of our class here because we can do that.So let’s grab this and we’re going to make it a public function in our point class.We’re going to modify it a little bit.We’re going to take out the parameters here because we don’t need this and we’re going to make it acast member function because it’s not going to be modifying our class here and we’re going to printX and Y directly, and this is going to do what we want.We’re going to get rid of this squiggly lines and what we need to do to print our object.We don’t need to call this guy directly like this.We can say p one and print info or print point and it is going to print our point here.But it doesn’t make to call this function yet because we haven’t really put in any data.So let’s just make it a member for now and we’re going to use it later.Let’s take this call out and we want to see if our Initializer List constructor was called.That’s what we want to see here.So let’s build our program.But before we do that, we’re going to clean up a little bit because now it’s ugly.Let’s build.With Jake.We’re going to see that the world is going to be good.So our constructor here must be valid and good.We can clear and run, Rooster.We’re going to see that the Initializer List constructor is called.So this is a step forward.Now that this guy is being called, we need to grab the data from the inside.Let’s see some initial things we can do on this list here.So the first thing we can do is print its size.We can print that out and say list size, and we can say list size.We can call the size method on this.And it is going to give us however many elements we have in our list here that was passed to us as aresult of the call here.Let’s build and run and see the size it should be.Two because we have two elements in our initializer here.So let’s clear and run Rooster.You’re going to see that the list size is two.And if you pass in more than two elements, the initializer list is going to take that.So let’s put in a 5.9, a 6.7 or 8 and a 4.3 here.This is going to do and you’re going to see that this is going to take it even if we have only two membervariables and the compiler trusts that we know what to do with the data that we are taking here becausewe built our own initializer list constructor.So if we print the size, we should see however many elements we have in here and I think it is fiveelements one, two, three, four, five.We should see a five printed out.So let’s build this and we’re going to clear and run Rooster.And you see that we have five elements.We are able to grab our data in our initializer and use that in our constructor here.This is pretty powerful.Okay, So now that we have this, we want to grab the first and second element because that’s what wewant to use to initialize our member variables here.To do this, we will have to use the begin pointer.That’s the list type here has.And to play with this, let’s do this.We’re going to print the first element.We’re going to save this and then we’re going to dereference the begin pointer.So we’re going to dereference and say list and call the begin method and this is going to dereferencethe pointer to the first element in the list here and it’s going to give us the value in there.So what we will get here in this case is 22.4 here, print it out.Let’s put in a new line character and we’re going to print this.Let’s build with GCC and we’re going to see our first element printed out.The build is good so we can run this.Let’s clear and run Rooster.We’re going to see that the first element is this guy here.We can even grab the second element.This is cool, right?So we can do stdcout and we’re going to say second element and we’re going to grab that.So we’re going to dereference the second element and we’re going to use Pointer arithmetic on this pointerhere.So we can say list begin and add one to that pointer.This is going to move us to the next spot.If we do this and print this out, we’re going to grab the second element and that’s going to be 65.4here.Let’s build and see that this is actually the case.So the world is good.We can clear and run.Rooster You see that 65.4 is our second element.We can even go on and keep printing stuff we have in our initializer.So for example, we can use the for loop.Let’s use that so we can do something like for size T and say I and initialize this to zero and we’regoing to keep looping as long as I is less than the size of our list.So we’re going to say I less than list size and then we’re going to increment, we’re going to put inour incrementing part, then we’re going to hit the body and then we’re going to print the element ata given index in this initializer list here.So we’re going to say stdcout and say element at index.We’re going to say the index.Let’s put I here and then we’re going to put a square bracket and we’re going to say which element itis and we’re going to use the pointer arithmetic notation.We’re going to Dereference and say list begin and we’re going to add the current iteration we are at.So we’re going to add I here and I think this is going to work.This is really cool, right?So we’re going to print all the elements that we have in our initializer, but notice that we’re goingto do that using a loop in the body of our Initializer List constructor.This is really cool.You can’t tell how excited I was when I learned about this.Let’s go out and see this working.We’re going to build with GCC.The build is good.We can clear.And run Rooster.We’re going to see that we have our elements in our initializer list here.We have from 22.4 that we have here, all the way to 4.3, which is the last element.And this is completely dynamic.If we change and put in more elements, let’s put in a 5.6 and do a 9.3 and do a 4.3.Let’s build and run and we’re going to see that the list here is going to grow dynamically.Notice that we didn’t need to change a thing and the body of our constructor here, we are able to grabour initializers and use them to do stuff.So let’s build and really show you this.We’re going to use GCC to do this, so let’s clear and run Rooster.We’re going to see that we are grabbing everything here.Eight elements from 0 to 7 and the last one is 4.3 here.This is really cool.So now that we know how to do this, we can use this to our advantage and grab the first element anduse that to initialize our member variable X and use the second element to initialize the second element,which is Y.Here, let’s do that.We’re going to comment all this out because I don’t want to take this out.I think it is really cool.So let’s keep this down in here in case you need to use this as a reference.And we’re just going to grab the first element and store that in our X member variable.So we’re going to dereference the begin pointer.We’re going to say list begin and that’s going to be our first element and we’re going to store thatin X, We’re going to grab the second element and store that in our Y member variable.Let’s do this and we’re going to use pointer arithmetic because I think it is good we’re going to addone because that’s going to be the second element.The element at index one is going to be what we want, which is going to be 65.4 here and we’re goingto use that to initialize our Y member variable.So by this point, if we go down and print information about P one, we’re going to say print point.We should see all the member variables printed out.We’re going to print what we have in X and we’re going to print what we have in Y, and you’re goingto see that it is the two pieces of data we have in the first spot and the second spot in our initializerhere.Let’s build and run.And you’re going to see this.We’re going to use GCC, as always.You see, the build is good.We’re going to clear and run Rooster.You see that?Our point is really working and it is exactly what we saw with the default initializer list that wasgenerated by the compiler for us.But now we have our own initializer list constructor and we can really make it do whatever we want.For example, we can flip all these two things and make X take the second element and we can do this.So if this makes sense for whatever application you are designing, please use this to your advantage.You already know how to do this.So if you look at this, you’re going to see that what we pass in here 22 is going to be used to initializeour Y member variable.So you can really customize how your initializer list constructors work.Let’s take this back to X and Y because I think what we just did is confusing, but hopefully this provesthat you can do all kinds of crazy things in your initializer list.Constructors.Okay.Before I let you go, let’s add some safety mechanism on our point class here and throw a compiler error.If people really pass more than two elements in our initializer list here because we don’t really wantmore.This is a point.And the point is going to have two member variables, and that’s what we want to take from this here.No more, no less.We just want two elements in our initializer here.We can enforce this by using a staticassert.We already know that.So what we can do, I think some of you are going to believe this can work is use a staticassert.But I hate to break to you.This is not going to work as you see here, because the size of our list is the runtime thing.It is something that is known at runtime when somebody initializes your object.So we can’t use this at compile time.It is not a constant expression.What we really need to use is something that works at runtime.So we’re going to use an assert.I don’t think we have seen what an assert is, but it is a mechanism.We have to check for things at runtime and if our check fails, we’re going to throw a compiler error.Let’s do that.We’re going to include a utility which is going to allow us to do that.So we’re going to include C assert and it is a library that is going to give us access to what we areabout to use and what we’re going to do is.A version of the Staticassert here, but it is going to be a runtime thing which is going to be evaluatedwhen your application is running.So we’re going to say assert.We can save that and inside we’re going to put in our assert expression here.So we’re going to basically be checking if the size of the list is two.If it’s not two, we’re going to throw a compiler error and we’re going to try and build our programhere.We’re going to see our compiler error.We’re not going to get a compiler error, but we’re going to get a runtime error when our code startsto run.So I have to correct this.Sorry, I was wrong about this.So we’re going to clear and run Rooster, and if we try to run it, you’re going to see that our programcrashed and it is telling us what is wrong here.It is saying list size equal to.So this has failed and it is going to tell us where this actually failed in our program.So, for example, we can go in our code and see the main CPP file and it’s going to give us the linenumber.And this is really cool.So we can go back there and look in the main CPP file, look at line eight and see what failed and wewill know that we have passed more than what is needed in this initializer here.If we change this to two elements, our code is going to build and do what we want.But note that this assert is going to check for things at runtime.When your application is running, let’s build and run.You’re going to see that this is going to work.If we pass less than two elements, we’re going to fail at runtime.So let’s build again.You’re going to see that the build is good.We can clear and run.Rooster and assertion is going to fail.The list size should be two and we only have one element in and our program is going to basically crashhere at line eight.We should fix this problem in our class.We’re going to look at this assert and we’re going to say if the size of the list is not two, we’renot throwing a compiler error.We are going to terminate the program with an error.Let’s say that I think this is more descriptive for this.So we’re going to see the problem here and we’re going to fix it and put in two elements.Let’s put in 34.9.Why not?And we’re going to build and this is going to work.Now, this is pretty cool.Let’s clear and run, Rooster.We’re going to see that we have our data in here.This is really all we set out to do in this lecture.Hopefully, you know that you can use initializer list constructors to your advantage.We are going to stop here in this lecture.In the next one, we’re going to learn about aggregate initialization.Go ahead and finish up here and and meet me there.


🤖 Assistant

C++ 聚合体的大括号初始化与自定义初始化列表构造函数(Initializer List Constructors for Aggregates)

简介: 本节课我们将深入学习C++中用于初始化聚合体的特殊构造函数——初始化列表构造函数。我们将探讨聚合体的概念,编译器如何默认支持这种初始化,以及如何编写自己的初始化列表构造函数来定制行为并进行运行时检查。


1. 什么是聚合体(Aggregates)?以及为什么需要初始化列表构造函数?

在C++中,聚合体是一种特殊的容器类型,它们能够“聚合”或包含许多小的数据项。你可以把它想象成一个大盒子,里面放着许多小盒子。

聚合体的特性和例子:

  • 本质:聚合体是一种将多个独立的数据项聚集在一起的复合类型。
  • 具体定义:C++对聚合体有严格的定义,但对于初学者,你可以暂时将其理解为一种可以容纳其他数据的容器。
  • 常见例子
    • 数组:例如 int arr[] = {1, 2, 3}; 数组就是最典型的聚合体。
    • 结构体:如果一个struct只包含公共的(或者说默认是公共的)非静态数据成员,没有用户定义的构造函数、析构函数、赋值运算符,也没有基类或虚函数等,那么它就是一个聚合体。
  • 统一的初始化语法:对于这些聚合体,C++提供了一种统一且非常简洁的初始化语法,即使用大括号 {}
    • 例如:Point p = {10.0, 20.0}; 或者 int arr[] = {1, 2, 3};
  • 编译器的工作:当你使用这种大括号初始化语法时,编译器会“知道”如何生成必要的工具来将这些数据放入你的容器中。它会根据数据在括号中的顺序,将它们依次存入聚合体的成员中。

为什么是初始化列表构造函数?

当编译器看到你使用大括号 {} 来初始化一个对象时,它实际上会在幕后(behind the scenes)调用或生成一个特殊的构造函数来处理这些数据。这个构造函数就是初始化列表构造函数

对于像struct Point { double x; double y; }; 这样的聚合体,即使你没有显式定义任何构造函数,编译器也会为你生成一个默认的初始化列表构造函数。这个默认行为通常是:按成员声明的顺序,将大括号中的数据依次赋给对应的成员。例如,Point p = {10.0, 20.0}; 会将 10.0 赋给 x20.0 赋给 y

2. 自定义初始化列表构造函数:std::initializer_list

虽然编译器可以为我们生成默认的初始化列表构造函数,但有时我们需要改变这种默认行为,例如:

  • 改变初始化顺序:你可能想让第一个传入的值初始化 y,第二个值初始化 x
  • 添加校验逻辑:你可能希望确保用户传入的数据项数量符合预期。
  • 执行自定义操作:你可能需要根据传入的列表执行一些额外的逻辑。

为了实现这些自定义,我们可以显式地定义自己的初始化列表构造函数。

语法结构:

一个自定义的初始化列表构造函数通常长这样:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <initializer_list> // 必须包含这个头文件

class MyClass {
public:
// ... 其他成员 ...

// 初始化列表构造函数
// 构造函数名与类名相同
// 参数类型为 std::initializer_list<T>,其中 T 是列表元素的类型
MyClass(std::initializer_list<double> list_param) {
// 在这里处理 list_param 中的数据
// 例如:
// if (list_param.size() >= 1) {
// member1 = *(list_param.begin());
// }
// if (list_param.size() >= 2) {
// member2 = *(list_param.begin() + 1);
// }
}

// ... 其他成员 ...
};

std::initializer_list<T> 的作用:

  • 模板类std::initializer_list 是C++标准库中的一个模板类。它用来表示一个不可变(immutable)的同类型元素的序列。
  • 类型匹配std::initializer_list<double> 中的 double 类型应该与你希望它接收的列表元素类型相匹配。如果你的类成员是 double,那么这里通常也用 double
  • 编译器转换:当你使用大括号 {} 初始化你的类对象时(例如 MyClass obj = {v1, v2, v3};),编译器会捕获 {v1, v2, v3} 中的所有数据,并用它们来构造一个临时的 std::initializer_list<T> 对象。然后,这个 std::initializer_list 对象会被作为参数传递给你定义的初始化列表构造函数。

3. 如何访问 std::initializer_list 中的元素?

std::initializer_list 提供了几种方法来访问它所包含的元素:

  • size() 方法:返回列表中元素的数量。
    • 例如:list_param.size()
  • 迭代器 (begin(), end())
    • list_param.begin():返回一个指向列表中第一个元素的迭代器(你可以简单地把它想象成一个指针)。
    • list_param.end():返回一个指向列表中“最后一个元素之后”位置的迭代器。这个迭代器不指向任何实际元素,它通常用于循环的终止条件。
  • 解引用运算符 *:如果你有一个迭代器(或指针),可以使用 * 来获取它所指向的实际值。
    • 例如:*list_param.begin() 将获取列表中的第一个元素的值。
  • 指针算术:虽然 begin() 返回的是迭代器,但对于 std::initializer_list 的迭代器,你通常可以像对待指针一样对它们执行加减运算,以访问后续元素。
    • 例如:*(list_param.begin() + 1) 将获取列表中的第二个元素的值(索引为1)。

示例代码(如何在构造函数中使用):

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <initializer_list> // 必须包含
#include <cassert> // 用于 assert

// 1. 定义一个 Point 结构体
struct Point {
private: // 通常我们将成员变量设为私有
double m_x;
double m_y;

public:
// 默认构造函数
Point() : m_x(0.0), m_y(0.0) {
std::cout << "默认构造函数 called." << std::endl;
}

// 自定义初始化列表构造函数
Point(std::initializer_list<double> list) {
std::cout << "Initializer List 构造函数 called." << std::endl;

// 运行时断言:确保传入的元素数量是 2
// assert 是一个运行时检查,如果条件为假,程序会终止并报错
// 记得包含 <cassert> 头文件
assert(list.size() == 2 && "Point requires exactly 2 values (x, y) for initialization.");

// 使用迭代器和指针算术访问元素并初始化成员变量
// 注意:这里我们假设列表至少有两个元素(通过上面的 assert 保证)
m_x = *(list.begin()); // 第一个元素初始化 x
m_y = *(list.begin() + 1); // 第二个元素初始化 y

// 如果你希望颠倒顺序,可以这样:
// m_y = *(list.begin());
// m_x = *(list.begin() + 1);
}

// 打印 Point 信息的方法
void print_info() const { // const 成员函数表示不修改对象状态
std::cout << "Point: [x=" << m_x << ", y=" << m_y << "]" << std::endl;
}
};

int main() {
std::cout << "--- 编译器生成的默认聚合初始化 ---" << std::endl;
// 对于满足聚合体定义的结构体,即使没有自定义构造函数,也可以这样初始化
// 注意:如果 Point 内部有自定义构造函数,它就不再是严格意义上的聚合体,
// 但是大括号初始化仍然有效,会调用对应的构造函数。
// 在我们这里,Point 有了自定义初始化列表构造函数,所以它会调用它。
Point p1 = {10.5, 20.5}; // 调用我们自定义的 Initializer List 构造函数
p1.print_info();
std::cout << std::endl;

std::cout << "--- 使用 Initializer List 构造函数初始化 ---" << std::endl;
Point p2({-5.0, 15.0}); // 显式使用大括号初始化,也会调用 Initializer List 构造函数
p2.print_info();
std::cout << std::endl;

std::cout << "--- 尝试传入过多元素 (会触发 assert 终止程序) ---" << std::endl;
// Point p3 = {1.0, 2.0, 3.0}; // 这行代码如果执行,会因为 assert 失败而终止程序

std::cout << "--- 尝试传入过少元素 (会触发 assert 终止程序) ---" << std::endl;
// Point p4 = {100.0}; // 这行代码如果执行,会因为 assert 失败而终止程序

Point p5; // 调用默认构造函数
p5.print_info();

return 0;
}

遍历 std::initializer_list 中的所有元素:

虽然对于 Point 这样固定数量成员的类,我们通常只取前几个元素,但 std::initializer_list 支持遍历所有元素。

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <initializer_list>
#include <cassert>

class MyCustomContainer {
private:
// 假设我们有一个动态数组来存储元素
double* m_data;
size_t m_size;

public:
MyCustomContainer(std::initializer_list<double> list) : m_size(list.size()) {
std::cout << "MyCustomContainer Initializer List 构造函数 called. List size: " << list.size() << std::endl;
m_data = new double[m_size]; // 分配内存

size_t i = 0;
// 方法1: 使用基于范围的 for 循环 (Range-based for loop)
// 这种方式最简洁、推荐使用
for (double val : list) {
m_data[i++] = val;
}

// 方法2: 使用传统 for 循环和迭代器/指针算术
// size_t i = 0;
// for (auto it = list.begin(); it != list.end(); ++it) {
// m_data[i++] = *it;
// }

// 方法3: 使用传统 for 循环和索引 (通过指针算术)
// for (size_t j = 0; j < list.size(); ++j) {
// m_data[j] = *(list.begin() + j);
// }
}

// 析构函数,释放内存
~MyCustomContainer() {
delete[] m_data;
m_data = nullptr;
}

void print_elements() const {
std::cout << "Elements: [";
for (size_t i = 0; i < m_size; ++i) {
std::cout << m_data[i] << (i == m_size - 1 ? "" : ", ");
}
std::cout << "]" << std::endl;
}
};

int main() {
MyCustomContainer c1 = {1.1, 2.2, 3.3, 4.4, 5.5};
c1.print_elements();

MyCustomContainer c2 = {100.0};
c2.print_elements();

return 0;
}

4. QA 闪存卡(QA Flash Cards)

Q1:C++ 中的聚合体是什么?
A1:聚合体是指符合特定条件的类类型,通常是不包含用户声明的构造函数、析构函数、虚函数、私有/保护非静态数据成员、基类等的结构体,以及数组类型。它们可以使用大括号初始化语法进行初始化。

Q2:初始化列表构造函数的目的是什么?
A2:它的目的是允许你使用统一的大括号初始化语法 {} 来构造和初始化对象,并为这种初始化提供自定义逻辑,而不仅仅依赖于编译器生成的默认行为。

Q3:如何自定义一个初始化列表构造函数
A3:通过定义一个接受 std::initializer_list<T> 类型参数的构造函数,其中 T 是你希望列表元素具有的类型。例如:MyClass(std::initializer_list<int> list_param);

Q4:如何在 std::initializer_list 中访问元素?
A4:可以使用 list.size() 获取元素数量;使用 *(list.begin()) 获取第一个元素;使用 *(list.begin() + N) 获取第 N+1 个元素(指针算术);或者使用基于范围的 for 循环遍历所有元素。

Q5:static_assertassert 有什么区别?
A5:

  • static_assert 用于编译期检查,它要求其条件是一个常量表达式。如果条件为假,则会导致编译错误。
  • assert 用于运行期检查,它在其条件为假时会终止程序的执行(通常是抛出异常或调用 abort())。它通常用于调试目的,在发布版本中可以被禁用。

5. 常见误解与错误(Common Misunderstandings or Mistakes)

  1. 混淆 std::initializer_list 与普通多参数构造函数
    • 错误:认为 Point(double x_val, double y_val) 也是一个初始化列表构造函数。
    • 澄清:虽然看起来相似,但只有参数类型是 std::initializer_list<T> 的构造函数才是初始化列表构造函数。它允许你使用统一的 {} 语法,并且可以接受任意数量的元素(只要类型匹配)。
  2. 试图对 std::initializer_list::size() 使用 static_assert
    • 错误:在初始化列表构造函数中写 static_assert(list.size() == 2, "Error message");
    • 澄清list.size() 的值是在运行时才知道的,因为它取决于用户实际传入了多少个元素。static_assert 只能用于编译期已知的值。对于运行时检查,应该使用 assert
  3. 忘记包含必要的头文件
    • 错误:使用 std::initializer_list 但没有包含 <initializer_list>。使用 assert 但没有包含 <cassert>
    • 澄清:这些都是标准库组件,必须显式包含对应的头文件。
  4. 未处理 std::initializer_list 中元素数量不匹配的情况
    • 错误:直接使用 *(list.begin() + 1) 访问第二个元素,但用户只传入了一个元素,导致未定义行为
    • 澄清:在使用 begin()指针算术访问元素之前,务必检查 list.size() 来确保有足够的元素,或者使用 assert 进行断言检查。对于可变数量的元素,应该遍历列表而不是固定索引访问。
  5. 认为 std::initializer_list 只能用于聚合体
    • 错误:只有数组结构体才能使用 std::initializer_list 构造函数。
    • 澄清:任何classstruct)都可以定义一个接受 std::initializer_list 的构造函数,从而允许使用大括号初始化语法来构造该类的对象,这大大增加了类的灵活性和可用性。
  6. 在构造函数体内进行成员初始化
    • 错误:在构造函数体内使用 x = *(list.begin());
    • 澄清:虽然可以,但更推荐使用成员初始化列表来初始化成员变量。例如:Point(std::initializer_list<double> list) : m_x(*(list.begin())), m_y(*(list.begin() + 1)) { ... } 这样可以避免先默认构造再赋值的开销,尤其对于复杂对象。然而,对于 std::initializer_list 本身,通常需要在构造函数体内进行处理,因为它是参数。这里特指我们自己的成员变量 m_x, m_y

6. 编程练习(Coding Exercise)

现在,请你来完成一个简单的练习。

任务:创建一个名为 Rectangle 的结构体,它代表一个二维矩形。

  • Rectangle 应该有两个 double 类型的私有成员变量:m_width (宽度) 和 m_height (高度)。
  • 它应该有一个自定义初始化列表构造函数,接受一个 std::initializer_list<double> 参数。
  • 这个构造函数应该:
    1. 打印一条消息,表明它被调用了。
    2. 使用 assert 确保传入的初始化列表恰好包含两个 double 值(分别用于宽度和高度)。如果不是,则程序终止。
    3. 使用指针算术解引用从列表中获取第一个值并初始化 m_width,获取第二个值并初始化 m_height
  • 添加一个公共的 print_dimensions() 方法来打印矩形的宽度和高度。
  • main 函数中,创建 Rectangle 对象:
    1. 成功初始化一个 Rectangle 对象。
    2. (可选)尝试用不正确的元素数量初始化一个 Rectangle 对象,观察 assert 的行为(记得注释掉这行代码,否则程序会终止)。

请在下面的代码块中填写你的解决方案:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <initializer_list>
#include <cassert> // Don't forget this for assert!

// 1. Define the Rectangle struct here
struct Rectangle {
private:
double m_width;
double m_height;

public:
// 2. Add your custom initializer list constructor here
// Remember to check list size and initialize m_width and m_height
// 提示:可以使用成员初始化列表来初始化成员变量
// Rectangle(std::initializer_list<double> list) : ________, ________ {
// // ... your assert and print message here ...
// }

// 3. Add the print_dimensions() method here
void print_dimensions() const {
std::cout << "Rectangle: [Width=" << m_width << ", Height=" << m_height << "]" << std::endl;
}
};

int main() {
// Test case 1: Correct initialization
Rectangle rect1 = {10.0, 5.0};
rect1.print_dimensions();

std::cout << std::endl;

// Test case 2 (Optional): Incorrect number of elements - Uncomment to see assert in action
// Rectangle rect2 = {20.0}; // Should trigger assert
// rect2.print_dimensions();

// Test case 3 (Optional): Incorrect number of elements - Uncomment to see assert in action
// Rectangle rect3 = {30.0, 15.0, 2.0}; // Should trigger assert
// rect3.print_dimensions();

return 0;
}

🧑‍💻 User

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <initializer_list>
#include <cassert> // Don't forget this for assert!

// 1. Define the Rectangle struct here
struct Rectangle {
private:
double m_width;
double m_height;

public:
// 2. Add your custom initializer list constructor here
// Remember to check list size and initialize m_width and m_height
// 提示:可以使用成员初始化列表来初始化成员变量
// Rectangle(std::initializer_list<double> list) : ________, ________ {
// // ... your assert and print message here ...
// }
Rectangle(std::initializer_list<double> list)
{
std::cout << "Initializer List called." << std::endl;
assert(list.size() == 2 && "list size shall be two to include width and length");
m_width = *(list.begin());
m_height = *(list.begin() + 1);
}

// 3. Add the print_dimensions() method here
void print_dimensions() const {
std::cout << "Rectangle: [Width=" << m_width << ", Height=" << m_height << "]" << std::endl;
}
};

int main() {
// Test case 1: Correct initialization
Rectangle rect1 = {10.0, 5.0};
rect1.print_dimensions();

std::cout << std::endl;

// Test case 2 (Optional): Incorrect number of elements - Uncomment to see assert in action
// Rectangle rect2 = {20.0}; // Should trigger assert
// rect2.print_dimensions();

// Test case 3 (Optional): Incorrect number of elements - Uncomment to see assert in action
// Rectangle rect3 = {30.0, 15.0, 2.0}; // Should trigger assert
// rect3.print_dimensions();

return 0;
}

🤖 Assistant

好的,你的解决方案已收到!作为你的C++专业导师,我将仔细审查你的代码,提供详细的反馈,并给出一些扩展的思考。

7. 评估用户解决方案并给出实际答案和解释

解决方案评分

总分:5/5

评价: 你的解决方案非常出色!你成功地实现了所有要求,包括定义结构体、实现初始化列表构造函数、使用 assert 进行运行时校验,并通过指针算术正确地初始化了成员变量。代码逻辑清晰,功能实现完整。

具体得分点:

  • Rectangle 结构体定义与成员变量私有化 (1/1):你正确地将 m_widthm_height 声明为私有成员,这是一个良好的封装实践。
  • 自定义初始化列表构造函数 (1/1):你成功地定义了接受 std::initializer_list<double> 参数的构造函数,这是本节课的核心内容。
  • 打印调用信息 (1/1):构造函数中包含了 std::cout << "Initializer List called." << std::endl;,符合要求。
  • 使用 assert 进行运行时校验 (1/1):你正确地使用了 assert(list.size() == 2 && "list size shall be two to include width and length"); 来检查列表大小,并给出了有用的错误信息。
  • 正确初始化成员变量 (1/1):你使用 m_width = *(list.begin());m_height = *(list.begin() + 1); 来初始化成员,精确地利用了指针算术和解引用。
  • print_dimensions() 方法 (1/1):你实现了正确的打印方法,并且是 const 成员函数,表示它不会修改对象状态,这也是一个好习惯。
  • main 函数测试 (1/1)main 函数中的测试用例也演示了正确的初始化。

你唯一没有完全按照提示(“可以使用成员初始化列表来初始化成员变量”)做的地方是:你在构造函数体内对 m_widthm_height 进行了赋值,而不是在成员初始化列表中。这并非错误,但涉及到C++的一个重要最佳实践,我们会在下面的“实际答案和解释”中详细说明。

实际答案和解释

你的代码是完全正确的,并且能够正常工作。这里我提供一个稍作优化的版本,主要是在构造函数中使用了成员初始化列表

优化的代码:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <initializer_list>
#include <cassert> // Don't forget this for assert!

// 1. Define the Rectangle struct here
struct Rectangle {
private:
double m_width;
double m_height;

public:
// 2. Add your custom initializer list constructor here
// 使用成员初始化列表初始化 m_width 和 m_height 是更推荐的做法
Rectangle(std::initializer_list<double> list)
: m_width(*(list.begin())), // 在这里初始化 m_width
m_height(*(list.begin() + 1)) // 在这里初始化 m_height
{
std::cout << "Initializer List Constructor called." << std::endl; // 稍微修改了打印信息

// 运行时断言:确保传入的元素数量是 2
assert(list.size() == 2 && "Rectangle requires exactly 2 values (width, height) for initialization.");

// 注意:由于成员变量已在成员初始化列表中初始化,这里不再需要赋值操作
// m_width = *(list.begin());
// m_height = *(list.begin() + 1);
}

// 3. Add the print_dimensions() method here
void print_dimensions() const {
std::cout << "Rectangle: [Width=" << m_width << ", Height=" << m_height << "]" << std::endl;
}
};

int main() {
// Test case 1: Correct initialization
Rectangle rect1 = {10.0, 5.0}; // 使用大括号初始化语法
rect1.print_dimensions();

std::cout << std::endl;

// Test case 2 (Optional): Incorrect number of elements - Uncomment to see assert in action
// Rectangle rect2 = {20.0}; // Should trigger assert and terminate the program
// rect2.print_dimensions();

// Test case 3 (Optional): Incorrect number of elements - Uncomment to see assert in action
// Rectangle rect3 = {30.0, 15.0, 2.0}; // Should trigger assert and terminate the program
// rect3.print_dimensions();

return 0;
}

为什么推荐使用成员初始化列表

在C++中,构造函数有两种方式来为成员变量赋值:

  1. 在构造函数体内赋值 (你目前的做法):

    cpp
    1
    2
    3
    4
    5
    Rectangle(std::initializer_list<double> list) {
    // ...
    m_width = *(list.begin());
    m_height = *(list.begin() + 1);
    }

    这种方式的问题在于,对于非基本类型(如自定义类对象),在进入构造函数体之前,它们的默认构造函数会先被隐式调用(如果存在)。然后,在构造函数体内,你再对它们进行一次赋值操作。这意味着可能会有一次不必要的默认构造和随后的赋值开销。

  2. 使用成员初始化列表 (推荐做法):

    cpp
    1
    2
    3
    4
    5
    6
    Rectangle(std::initializer_list<double> list) 
    : m_width(*(list.begin())),
    m_height(*(list.begin() + 1))
    {
    // ... 构造函数体
    }

    这种方式直接在成员变量被创建时就使用指定的值进行初始化。这通常效率更高,因为它避免了先进行默认构造再进行赋值的步骤。对于某些没有默认构造函数的类类型成员,或者 const 成员、引用成员,你必须使用成员初始化列表来初始化它们。

虽然对于像 double 这样的基本类型,这种差异通常可以忽略不计,但养成使用成员初始化列表的好习惯对编写高效和正确的C++代码非常重要。你的代码的执行顺序是:

  1. list.begin()list.begin() + 1 被求值。
  2. m_width 被初始化为第一个值。
  3. m_height 被初始化为第二个值。
  4. 构造函数体内的 std::cout 语句执行。
  5. assert 语句执行。

这确保了成员变量在构造函数体开始执行之前就已经被初始化了。

8. 扩展问题和建议(Extended Questions and Suggestions)

你已经很好地掌握了初始化列表构造函数的基本概念和用法。接下来,你可以探索以下几个方面,加深对相关知识的理解:

  1. 成员初始化列表的深入理解

  2. std::initializer_list 的其他应用

    • 容器的初始化:许多标准库容器,如 std::vector, std::map, std::set 等,都提供了接受 std::initializer_list 的构造函数。你可以尝试用这种方式初始化一个 std::vector<double>
    • 可变参数函数:思考如何设计一个函数,它能接受任意数量的相同类型参数,而不仅仅是固定两个。std::initializer_list 是实现这种“可变参数”的一种安全且类型安全的方式。
    • 设计通用容器:如果让你设计一个简单的动态数组类(类似 std::vector),如何利用 std::initializer_list 来实现它的便捷初始化?这需要你处理列表中元素的复制和存储,并考虑内存管理。
  3. 运行时检查与异常处理

    • 目前你使用了 assert,它在调试模式下很有用,但在发布模式下通常会被禁用,导致错误可能不被发现或者程序崩溃。
    • 学习C++的异常处理机制(try, catch, throw)。尝试将 assert 替换为抛出 std::invalid_argument 异常,并学习如何在 main 函数中捕获并处理这个异常,而不是让程序终止。这在实际软件开发中是更健壮的做法。
  4. 聚合体的详细定义

    • 如果你有兴趣,可以深入了解C++标准中对聚合体的精确定义。这有助于你理解为什么某些可以进行大括号初始化而另一些则不行。

继续保持这种学习热情,你会在C++的学习道路上取得更大进步!


评论